package org.force4spring;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.axis.components.logger.LogFactory;
import org.apache.axis.session.Session;
import org.apache.commons.logging.Log;
import org.force4spring.support.ForceSession;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.orm.hibernate3.HibernateTransactionManager;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import com.sforce.soap.partner.fault.ApiFault;

/**
 * This interceptor binds a new Force.com Session to the thread before a method
 * call, closing and removing it afterwards in case of any method outcome. If
 * there already is a pre-bound Session (e.g. from ForceTransactionManager, or
 * from a surrounding Force-intercepted method), the interceptor simply
 * participates in it.
 * 
 * <p>
 * Application code must retrieve a Force.com Session via the
 * <code>SessionFactoryUtils.getSession</code> method, to be able to detect a
 * thread-bound Session. It is preferable to use <code>getSession</code> with
 * allowCreate=false, if the code relies on the interceptor to provide proper
 * Session handling. Typically, the code will look like as follows:
 * 
 * <pre>
 * public void doSomeDataAccessAction() {
 *   Session session = SessionFactoryUtils.getSession(this.sessionFactory, false);
 *   try {
 *     ...
 *   }
 *   catch (ApiFault fault) {
 *     throw SessionFactoryUtils.convertApiFault(fault);
 *   }
 * }
 * </pre>
 * 
 * Note that the application must care about handling ApiFault itself,
 * preferably via delegating to the
 * <code>SessionFactoryUtils.convertApiFault</code> method that converts them
 * to exceptions that are compatible with the
 * <code>org.springframework.dao</code> exception hierarchy (like
 * ForceTemplate does).
 * 
 * <p>
 * Unfortunately, this interceptor cannot convert checked ApiFault to unchecked
 * dao ones transparently. The intercepted method would have to declare the
 * checked ApiFault - thus the caller would still have to catch or rethrow it,
 * even if it will never be thrown if intercepted. Any such exception will
 * nevertheless get converted by default.
 * 
 * <p>
 * This class can be considered a declarative alternative to ForceTemplate's
 * callback approach. The advantages are:
 * <ul>
 * <li>no anonymous classes necessary for callback implementations;
 * <li>the possibility to throw any application exceptions from within data
 * access code.
 * </ul>
 * 
 * <p>
 * The drawbacks are:
 * <ul>
 * <li>the dependency on interceptor configuration;
 * <li>the delegating try/catch blocks.
 * </ul>
 * 
 * @author Juergen Hoeller
 * @author Max Rudman
 * @see SessionFactoryUtils#getSession
 * @see HibernateTransactionManager
 * @see HibernateTemplate
 */
public class ForceInterceptor extends ForceAccessor implements
		MethodInterceptor {
	private static final Log log = LogFactory.getLog(ForceInterceptor.class
			.getName());

	private boolean exceptionConversionEnabled = true;

	/**
	 * Set whether to convert any ApiFault raised to a Spring
	 * DataAccessException, compatible with the
	 * <code>org.springframework.dao</code> exception hierarchy.
	 * <p>
	 * Default is "true". Turn this flag off to let the caller receive raw
	 * exceptions as-is, without any wrapping. Note that this means that the DAO
	 * methods will have to declare the checked ApiFault, and callers will be
	 * forced to handle it.
	 * 
	 * @see org.springframework.dao.DataAccessException
	 */
	public void setExceptionConversionEnabled(boolean exceptionConversionEnabled) {
		this.exceptionConversionEnabled = exceptionConversionEnabled;
	}

	public Object invoke(MethodInvocation methodInvocation) throws Throwable {
		ForceSession session = getSession();
		boolean existingTransaction = SessionFactoryUtils
				.isSessionTransactional(session, getSessionFactory());

		if (existingTransaction) {
			log.debug("Found thread-bound Session for ForceInterceptor");
		} else {
			TransactionSynchronizationManager.bindResource(getSessionFactory(),
					new SessionHolder(session));
		}

		try {
			Object retVal = methodInvocation.proceed();
			flushIfNecessary(session, existingTransaction);
			return retVal;
		} catch (ApiFault fault) {
			if (this.exceptionConversionEnabled) {
				throw convertApiFault(fault);
			} else {
				throw fault;
			}
		} finally {
			if (existingTransaction) {
				log
						.debug("Not closing pre-bound Force Session after ForceInterceptor");
			} else {
				TransactionSynchronizationManager
						.unbindResource(getSessionFactory());
				SessionFactoryUtils.closeSessionOrRegisterDeferredClose(
						session, getSessionFactory());
			}
		}
	}

	/**
	 * Return a Session for use by this interceptor.
	 * 
	 * @see SessionFactoryUtils#getSession
	 */
	protected ForceSession getSession() {
		return SessionFactoryUtils.getSession(getSessionFactory());
	}

}
